const t = require('../test-lib/test.js');
const assert = require('assert/strict');
const fs = require('fs');
const fsp = require('fs/promises');
const path = require('path');
const FormData = require('form-data');

const publicFolderPath = path.join(process.cwd(), 'test/public');

describe('Images', function() {

  let apos;
  let jar;
  let inserted;
  let image;

  const mockImages = [
    {
      type: '@apostrophecms/image',
      slug: 'image-1',
      visibility: 'public',
      attachment: {
        extension: 'jpg',
        width: 500,
        height: 400
      }
    },
    {
      type: '@apostrophecms/image',
      slug: 'image-2',
      visibility: 'public',
      attachment: {
        extension: 'jpg',
        width: 500,
        height: 400
      }
    },
    {
      type: '@apostrophecms/image',
      slug: 'image-3',
      visibility: 'public',
      attachment: {
        extension: 'jpg',
        width: 150,
        height: 150
      }
    },
    {
      type: '@apostrophecms/image',
      slug: 'image-4',
      visibility: 'public',
      attachment: {
        extension: 'svg'
      }
    }
  ];

  this.timeout(t.timeout);

  after(function() {
    return t.destroy(apos);
  });

  it('should be a property of the apos object', async function() {
    this.timeout(t.timeout);
    this.slow(2000);

    apos = await t.create({
      root: module
    });

    assert(apos.image);
    assert(apos.image.__meta.name === '@apostrophecms/image');
  });

  // Test pieces.list()
  it('should clean up any existing images for testing', async function() {
    try {
      const response = await apos.doc.db.deleteMany(
        { type: '@apostrophecms/image' }
      );
      assert(response.result.ok === 1);
    } catch (e) {
      assert(false);
    }
  });

  it('should add images for testing', async function() {
    assert(apos.image.insert);

    const req = apos.task.getReq();

    const insertPromises = mockImages.map(async (image) => {
      return apos.image.insert(req, image);
    });

    inserted = await Promise.all(insertPromises);

    assert(inserted.length === mockImages.length);
    assert(inserted[0]._id);
  });

  it('should respect minSize filter (svg is always OK)', async function() {
    const req = apos.task.getAnonReq();
    const images = await apos.image.find(req).minSize([ 200, 200 ]).toArray();

    assert(images.length === 3);
  });

  it('should respect minSize filter in toCount, which uses a cloned cursor', async function() {
    const req = apos.task.getAnonReq();
    const count = await apos.image.find(req).minSize([ 200, 200 ]).toCount();

    assert(count === 3);
  });

  it('should generate a srcset string for an image', function() {
    const srcset = apos.image.srcset({
      name: 'test',
      _id: 'test',
      extension: 'jpg',
      width: 1200,
      height: 800
    });

    assert.strictEqual(srcset, [ '/uploads/attachments/test-test.max.jpg 1200w',
      '/uploads/attachments/test-test.full.jpg 1140w',
      '/uploads/attachments/test-test.two-thirds.jpg 760w',
      '/uploads/attachments/test-test.one-half.jpg 570w',
      '/uploads/attachments/test-test.one-third.jpg 380w',
      '/uploads/attachments/test-test.one-sixth.jpg 190w' ].join(', '));
  });

  it('should not generate a srcset string for an SVG image', function() {
    const srcset = apos.image.srcset({
      name: 'test',
      _id: 'test',
      extension: 'svg',
      width: 1200,
      height: 800
    });

    assert.strictEqual(srcset, '');
  });

  it('should be able to insert test users', async function() {

    await insertUser({
      title: 'admin',
      username: 'admin',
      password: 'admin',
      email: 'ad@min.com',
      role: 'admin'
    });

    await insertUser({
      title: 'contributor',
      username: 'contributor',
      password: 'contributor',
      email: 'con@tributor.com',
      role: 'contributor'
    });

  });

  it('REST: should be able to log in as admin', async function() {
    jar = await login('admin');
  });

  it('"editable" API includes images for admin', async function() {

    const editable = await getEditableImages(jar);
    assert(editable.length === 4);
  });

  it('REST: should be able to log in as contributor', async function() {
    jar = await login('contributor');
  });

  it('"editable" API does not include images for contributor', async function() {
    const editable = await getEditableImages(jar);
    assert(editable.length === 0);
  });

  it('REST: should be able to upload an image with an attachment as an admin', async function() {
    jar = await login('admin');
    const formData = new FormData();
    formData.append('file', fs.createReadStream(path.join(apos.rootDir, '/public/test-image.jpg')));

    // Make an async request to upload the image.
    const attachment = await apos.http.post('/api/v1/@apostrophecms/attachment/upload', {
      body: formData,
      jar
    });

    image = await apos.http.post('/api/v1/@apostrophecms/image', {
      body: {
        title: 'Test Image',
        attachment
      },
      jar
    });
    assert(image);
    assert(image.title === 'Test Image');
  });

  it('REST: autocrop should have no effect when there are no widget options', async function() {
    const result = await apos.http.post('/api/v1/@apostrophecms/image/autocrop', {
      body: {
        relationship: [ image ],
        widgetOptions: {}
      },
      jar
    });
    assert(result.relationship);
    assert(result.relationship[0]);
    assert(result.relationship[0].title === 'Test Image');
    assert(!result.relationship[0]._fields);
  });

  it('REST: autocrop should work when aspectRatio is less than actual image', async function() {
    const result = await apos.http.post('/api/v1/@apostrophecms/image/autocrop', {
      body: {
        relationship: [ image ],
        widgetOptions: {
          aspectRatio: [ 1, 2 ]
        }
      },
      jar
    });
    assert(result.relationship);
    const output = result.relationship[0];
    assert(output);
    assert(output.title === 'Test Image');
    const fields = output._fields;
    assert(fields);
    // Useful for visual verification
    // require('child_process').execSync(`open
    // test/public${output.attachment._urls.full} &`);

    assert.strictEqual(fields.top, 0);
    assert.strictEqual(fields.left, 75);
    assert.strictEqual(fields.width, 300);
    assert.strictEqual(fields.height, 600);
  });

  it('REST: autocrop should work when aspectRatio is greater than actual image', async function() {
    const result = await apos.http.post('/api/v1/@apostrophecms/image/autocrop', {
      body: {
        relationship: [ image ],
        widgetOptions: {
          aspectRatio: [ 2, 1 ]
        }
      },
      jar
    });
    assert(result.relationship);
    const output = result.relationship[0];
    assert(output);
    assert(output.title === 'Test Image');
    const fields = output._fields;
    assert(fields);
    // Useful for visual verification
    // require('child_process').execSync(`open
    // test/public${output.attachment._urls.full} &`);
    assert.strictEqual(fields.top, 187);
    assert.strictEqual(fields.left, 0);
    assert.strictEqual(fields.width, 450);
    assert.strictEqual(fields.height, 225);
  });

  it('should update crop fields when replacing an image attachment', async function () {
    await t.destroy(apos);
    await fsp.rm(path.join(publicFolderPath, 'uploads'), {
      recursive: true,
      force: true
    });
    apos = await t.create({
      root: module,
      modules: {
        'test-piece': {
          extend: '@apostrophecms/piece-type',
          fields: {
            add: {
              main: {
                type: 'area',
                options: {
                  widgets: {
                    '@apostrophecms/image': {
                      aspectRatio: [ 3, 2 ]
                    }
                  }
                }
              }
            }
          }
        }
      }
    });
    await insertUser({
      title: 'admin',
      username: 'admin',
      password: 'admin',
      email: 'ad@min.com',
      role: 'admin'
    });

    // Upload an image (landscape), crop it, insert a piece with the cropped
    // image
    jar = await login('admin');
    const formData = new FormData();
    const stream = fs.createReadStream(
      path.join(apos.rootDir, '/public/test-image-landscape.jpg')
    );
    formData.append('file', stream);
    const attachment = await apos.http.post('/api/v1/@apostrophecms/attachment/upload', {
      body: formData,
      jar
    });
    stream.close();
    image = await apos.http.post('/api/v1/@apostrophecms/image', {
      body: {
        title: 'Test Image Landscape',
        attachment
      },
      jar
    });
    assert.equal(image._prevAttachmentId, attachment._id);
    const crop = await apos.http.post('/api/v1/@apostrophecms/image/autocrop', {
      body: {
        relationship: [ image ],
        widgetOptions: {
          aspectRatio: [ 3, 2 ]
        }
      },
      jar
    });
    let piece = await apos.http.post('/api/v1/test-piece', {
      jar,
      body: {
        title: 'Test Piece',
        slug: 'test-piece',
        type: 'test-piece',
        main: {
          metaType: 'area',
          items: [
            {
              type: '@apostrophecms/image',
              metaType: 'widget',
              imageIds: [ image.aposDocId ],
              imageFields: {
                [image.aposDocId]: crop.relationship[0]._fields
              },
              _image: [ crop.relationship[0] ]
            }
          ]
        }
      }
    });

    let imageFields = piece.main.items[0].imageFields[image.aposDocId];
    assert(imageFields, 'imageFields should be present when creating the piece');
    assert.equal(imageFields.width / imageFields.height, 3 / 2, 'aspect ratio should be 3:2');
    await fsp.access(
      path.join(
        publicFolderPath,
        attachment._urls.original.replace(
          '.jpg',
        `.${imageFields.left}.${imageFields.top}.${imageFields.width}.${imageFields.height}.jpg`
        )
      )
    );

    // Replace the image with portrait orientation, verify that the aspect
    // ratio is preserved
    const formDataPortrait = new FormData();
    const streamPortrait = fs.createReadStream(path.join(apos.rootDir, '/public/test-image.jpg'));
    formDataPortrait.append('file', streamPortrait);
    const attachmentPortrait = await apos.http.post('/api/v1/@apostrophecms/attachment/upload', {
      body: formDataPortrait,
      jar
    });
    image = await apos.http.put(`/api/v1/@apostrophecms/image/${image._id}`, {
      body: {
        title: 'Test Image Portrait',
        attachment: attachmentPortrait
      },
      jar
    });
    streamPortrait.close();
    piece = await apos.http.get(`/api/v1/test-piece/${piece._id}`, {
      jar
    });
    imageFields = piece.main.items[0].imageFields[image.aposDocId];
    assert(imageFields, 'imageFields should be present after replacing the image attachment');
    assert.equal(imageFields.width / imageFields.height, 3 / 2, 'aspect ratio should be 3:2');
    await fsp.access(
      path.join(
        publicFolderPath,
        attachmentPortrait._urls.original.replace(
          '.jpg',
        `.${imageFields.left}.${imageFields.top}.${imageFields.width}.${imageFields.height}.jpg`
        )
      )
    );
  });

  async function insertUser(info) {
    const user = apos.user.newInstance();
    assert(user);
    Object.assign(user, info);
    await apos.user.insert(apos.task.getReq(), user);
  }

  async function login(username, password) {
    if (!password) {
      password = username;
    }
    jar = apos.http.jar();

    // establish session
    let page = await apos.http.get('/', {
      jar
    });

    assert(page.match(/logged out/));

    // Log in

    await apos.http.post('/api/v1/@apostrophecms/login/login', {
      body: {
        username,
        password,
        session: true
      },
      jar
    });

    // Confirm login
    page = await apos.http.get('/', {
      jar
    });

    assert(page.match(/logged in/));
    return jar;
  }

  async function getEditableImages(jar) {
    return (await apos.http.post('/api/v1/@apostrophecms/doc/editable?aposMode=draft', {
      body: {
        ids: inserted.map(doc => doc._id.replace(':published', ':draft'))
      },
      jar
    })).editable;
  }
});

describe('Image Lib', function() {
  const imageLib = require('../lib/image.js');

  describe('computeMinSizes', function() {
    it('should return the min sizes if no aspect ratio is provided', function() {
      const result1 = imageLib.computeMinSizes([ 200, 100 ]);
      const result2 = imageLib.computeMinSizes([ 100, 200 ], 0); // ignore 0

      assert.deepEqual(result1, {
        minWidth: 200,
        minHeight: 100
      });
      assert.deepEqual(result2, {
        minWidth: 100,
        minHeight: 200
      });
    });

    it('should return the higher value for square aspect ratio', function() {
      const result1 = imageLib.computeMinSizes([ 200, 100 ], 1);
      const result2 = imageLib.computeMinSizes([ 100, 200 ], 1);

      assert.deepEqual(result1, {
        minWidth: 200,
        minHeight: 200
      });
      assert.deepEqual(result2, {
        minWidth: 200,
        minHeight: 200
      });
    });

    it('should compute the min sizes for a wider aspect ratio', function() {
      const result1 = imageLib.computeMinSizes([ 1000, 300 ], [ 3, 1 ]);
      const result2 = imageLib.computeMinSizes([ 500, 2000 ], [ 3, 1 ]);
      const result3 = imageLib.computeMinSizes([ 100, 100 ], [ 3, 1 ]);
      const result4 = imageLib.computeMinSizes([ 30, 50 ], [ 3, 1 ]);
      const result5 = imageLib.computeMinSizes([ 600, 1800 ], [ 3, 1 ]);
      const result6 = imageLib.computeMinSizes([ 1800, 600 ], [ 3, 1 ]);

      assert.deepEqual(result1, {
        minWidth: 1000,
        minHeight: 1000 / 3
      });
      assert.deepEqual(result2, {
        minWidth: 6000,
        minHeight: 2000
      });
      assert.deepEqual(result3, {
        minWidth: 300,
        minHeight: 100
      });
      assert.deepEqual(result4, {
        minWidth: 150,
        minHeight: 50
      });
      assert.deepEqual(result5, {
        minWidth: 5400,
        minHeight: 1800
      });
      assert.deepEqual(result6, {
        minWidth: 1800,
        minHeight: 600
      });
    });

    it('should compute the min sizes for a taller aspect ratio', function() {
      const result1 = imageLib.computeMinSizes([ 1000, 300 ], [ 1, 3 ]);
      const result2 = imageLib.computeMinSizes([ 500, 2000 ], [ 1, 3 ]);
      const result3 = imageLib.computeMinSizes([ 100, 100 ], [ 1, 3 ]);
      const result4 = imageLib.computeMinSizes([ 30, 50 ], [ 1, 3 ]);
      const result5 = imageLib.computeMinSizes([ 600, 1800 ], [ 1, 3 ]);
      const result6 = imageLib.computeMinSizes([ 1800, 600 ], [ 1, 3 ]);

      assert.deepEqual(result1, {
        minWidth: 1000,
        minHeight: 3000
      });
      assert.deepEqual(result2, {
        minWidth: 2000 / 3,
        minHeight: 2000
      });
      assert.deepEqual(result3, {
        minWidth: 100,
        minHeight: 300
      });
      assert.deepEqual(result4, {
        minWidth: 30,
        minHeight: 90
      });
      assert.deepEqual(result5, {
        minWidth: 600,
        minHeight: 1800
      });
      assert.deepEqual(result6, {
        minWidth: 1800,
        minHeight: 5400
      });
    });
  });
});
